<!--
 * @Description: 表单
 * @Author: HuiSir<https://gitee.com/huisir001>
 * @Date: 2021-11-04 16:39:23
 * @LastEditTime: 2025-09-28 17:27:31
-->
<template>
  <el-form
    ref="elForm"
    :model="modelValue"
    :class="{ disabled, 'edit-form': true }"
    :inline="inline"
    :disabled="disabled"
    :label-width="labelWidth"
    :style="`--form-width:${getStringWidth(formWidth)};--form-margin:${
      center ? '0 auto' : '0'
    }`"
  >
    <template v-for="item in options" :key="item.key">
      <el-form-item
        v-if="formItemShow(item, modelValue)"
        :label="item.label"
        :prop="item.key"
        :rules="setRulesMeg(item)"
        :label-width="item.labelWidth || labelWidth"
        :style="`--input-width:${
          getStringWidth(item.inputWidth) || myInputWidth
        }`"
      >
        <slot v-if="item.slot" :name="item.slot" />
        <template v-else>
          <!-- 日期，这里单独拎出来是因为使用动态组件的话会导致日期弹框BUG -->
          <el-date-picker
            v-if="item.component == 'date-picker'"
            :model-value="modelValue[item.key]"
            @update:modelValue="setModelVal($event, item.key)"
            v-bind="item.props"
            :placeholder="item.props?.placeholder || '请选择日期'"
            clearable
          >
          </el-date-picker>
          <!-- radio单选 -->
          <el-radio-group
            v-else-if="item.component == 'radio'"
            :model-value="modelValue[item.key]"
            @update:modelValue="setModelVal($event, item.key)"
            v-bind="item.props"
          >
            <template v-if="item.options">
              <el-radio
                v-for="option in item.options"
                :key="option.value"
                :value="option.value"
                v-bind="item.props"
                >{{ option.label }}</el-radio
              >
            </template>
          </el-radio-group>
          <!-- 富文本编辑器 -->
          <fy-rich-editor
            v-else-if="
              item.component == 'rich-editor' || item.component == 'richEditor'
            "
            :model-value="modelValue[item.key]"
            @update:modelValue="setModelVal($event, item.key)"
            v-bind="item.props"
          />
          <!-- 文件上传 -->
          <!-- 考虑到多文件上传，后端可能使用逗号分隔字符串实现，也可能使用数组，这里需要格式化 -->
          <fy-upload
            v-else-if="item.component === 'upload'"
            ref="fyUpload"
            :model-value="modelValue[item.key]"
            :formItemKey="item.key"
            @update:modelValue="setModelVal($event, item.key)"
            @upload:change="$emit('upload:change', $event)"
            v-bind="item.props"
          />
          <!-- 其他 -->
          <component
            v-else
            :is="`el-${item.component}`"
            :model-value="modelValue[item.key]"
            :autocomplete="
              item.props?.type === 'password' ? 'new-password' : 'off'
            "
            @update:modelValue="setModelVal($event, item.key)"
            v-bind="item.props"
            :placeholder="
              item.props?.placeholder ||
              (item.component === 'select' ? '请选择' : '请输入') + item.label
            "
            clearable
          >
            <!-- 下拉框 -->
            <template v-if="item.options">
              <el-option
                v-for="option in item.options"
                :key="option.value"
                :label="option.label"
                :value="option.value"
              ></el-option>
            </template>
          </component>
        </template>
        <span v-if="item.suffix" class="suffix">{{ item.suffix }}</span>
        <div v-if="item.desc" class="desc">
          {{
            typeof item.desc === "function" ? item.desc(modelValue) : item.desc
          }}
        </div>
      </el-form-item>
    </template>
    <el-form-item v-if="showBtns">
      <slot name="otherBtns" />
      <el-button type="primary" :loading="submitLoading" @click="submit">{{
        submitBtnLabel
      }}</el-button>
      <el-button v-if="showResetBtn" :loading="resetLoading" @click="reset"
        >重置</el-button
      >
    </el-form-item>
  </el-form>
</template>

<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { computed, defineComponent, PropType, ref, watch } from "vue";
import { IFormOption } from "../types";
import { throttle } from "../../helpers";
import { ElMessage } from "element-plus";

export default defineComponent({
  name: "FyForm",
  props: {
    /**
     * 是否行内
     */
    inline: {
      type: Boolean,
      require: false,
      default: true,
    },
    /**
     * 是否禁用全部
     */
    disabled: {
      type: Boolean,
      default: false,
    },
    /**
     * 是否显示按钮
     */
    showBtns: {
      type: Boolean,
      require: false,
      default: true,
    },
    /**
     * 是否展示重置按钮，`showBtns`为`true`时生效
     */
    showResetBtn: {
      type: Boolean,
      require: false,
      default: true,
    },
    /**
     * 提交按钮文本，默认为搜索
     */
    submitBtnLabel: {
      type: String,
      default: "搜索",
    },
    /**
     * 表单项配置选项
     */
    options: {
      type: Array as PropType<IFormOption[]>,
      required: true,
      default: () => [],
    },
    /**
     * v-model传参写法
     * ！！！！！
     * 特别注意：Vue3 `v-model`实现绑定对象时，绑定的数据若为`reactive`会导致在`update:modelValue`触发时丢失响应式
     * 所以一般在绑定`v-model`数据时，参数用`ref`类型
     * 若必须使用`reactive`，这里也做了兼容，在更改对象时先单个修改modelValue值，再触发`update:modelValue`事件
     * 且使用`reactive`时，定义变量使用`let`而非`const`
     * ！！！！！
     */
    modelValue: {
      type: Object as PropType<Record<string, any>>,
      required: true,
      default: () => ({}),
    },
    labelWidth: {
      type: [String, Number],
      require: false,
      default: 80,
    },
    inputWidth: {
      type: [String, Number],
      require: false,
      default: 220,
    },
    formWidth: {
      type: [String, Number],
      require: false,
      default: "100%",
    },
    /**
     * 是否居中，默认否
     * 全屏form时可居中
     */
    center: {
      type: Boolean,
      require: false,
      default: false,
    },
  },
  // `update:modelValue`为`v-model`传参固定事件写法
  emits: ["update:modelValue", "submit", "reset", "upload:change"],
  setup(props, { emit }) {
    // 表单对象
    const elForm = ref();

    // 文件上传对象
    const fyUpload = ref<{ submit: () => void; formItemKey: string }[]>([]);

    // 数据修改
    const setModelVal = (val: unknown, key: string) => {
      Object.assign(props.modelValue, { [key]: val });
      emit("update:modelValue", props.modelValue);
    };

    // 初始化默认值，这里只执行一次
    watch(
      () => props.options,
      (options) => {
        options.forEach((item) => {
          // eslint-disable-next-line vue/no-mutating-props
          props.modelValue[item.key] =
            props.modelValue[item.key] != void 0
              ? props.modelValue[item.key]
              : item.default != void 0
              ? item.default
              : null;
        });

        // 更新
        emit("update:modelValue", props.modelValue);
      },
      { once: true, immediate: true }
    );

    // 给出默认必填规则
    const setRulesMeg = (options: IFormOption) => {
      const { rules, label, props } = options;
      if (!rules) return;
      if (Array.isArray(rules)) {
        rules.forEach((item) => {
          if (item.required && !item.message) {
            item.message = label
              ? `${label}不能为空`
              : props?.placeholder || "该项为必填项";
          }
          if (!item.trigger) {
            item.trigger = "blur";
          }
        });
      } else {
        if (rules.required && !rules.message) {
          rules.message = label
            ? `${label}不能为空`
            : props?.placeholder || "该项为必填项";
        }
        if (!rules.trigger) {
          rules.trigger = "blur";
        }
      }
      return rules;
    };

    const getStringWidth = (width?: number | string) => {
      if (typeof width === "number") {
        return width + "px";
      }
      return width;
    };

    const myInputWidth = computed(() => getStringWidth(props.inputWidth));

    /**
     * 清空表单
     */
    const clear = () => {
      for (const key in props.modelValue) {
        Object.assign(props.modelValue, { [key]: null });
      }
      emit("update:modelValue", props.modelValue);
    };

    // 按钮loading
    const resetLoading = ref(false);
    const submitLoading = ref(false);

    /**
     * 提交
     */
    const submit = throttle(
      () => {
        // 节流
        emit("submit");
      },
      800,
      submitLoading
    );

    /**
     * 重置-节流操作
     */
    const reset = throttle(
      () => {
        // 节流
        clear();
        emit("reset");
      },
      800,
      resetLoading
    );

    /**
     * 文件上传若为手动执行上传，这里暴露方法
     */
    const uploadSubmit = async () => {
      const result = await Promise.all(
        fyUpload.value.map((upload) => upload.submit())
      );
      return result.map((item, ind) => ({
        key: fyUpload.value[ind].formItemKey,
        result: item,
      }));
    };

    /**
     * 表单验证
     */
    const validate = async () => {
      const valid = !!(await elForm.value.validate().catch(() => {
        submitLoading.value = false;
        ElMessage.error("输入有误，请检查");
      }));
      if (!valid) {
        submitLoading.value = false;
      }
      return valid;
    };

    /**
     * 判断是否展示表单项
     */
    const formItemShow = (opt: IFormOption, formData: Record<string, any>) => {
      const show =
        typeof opt.hide === "function" ? !opt.hide(formData) : !opt.hide;

      // 表单项隐藏时候清空数据
      if (!show && formData[opt.key]) {
        delete formData[opt.key];
      }

      return show;
    };

    return {
      elForm,
      validate,
      fyUpload,
      setModelVal,
      resetLoading,
      submitLoading,
      submit,
      reset,
      getStringWidth,
      myInputWidth,
      setRulesMeg,
      clear,
      uploadSubmit,
      formItemShow,
    };
  },
});
</script>

<style scoped lang="scss">
.edit-form {
  width: var(--form-width);
  margin: var(--form-margin);
  &.el-form--inline {
    :deep(.el-form-item) {
      vertical-align: sub;
    }
  }
  &:deep(.el-form-item) {
    .el-form-item__label {
      justify-content: flex-end;
      flex-direction: row-reverse;
    }

    .el-form-item__content {
      width: var(--input-width) !important;
      flex: unset !important;
      max-width: 100%;

      .suffix {
        margin-left: 8px;
      }

      .desc {
        font-size: 94%;
        color: var(--el-text-color-secondary);
        word-break: break-word;
        line-height: 1.2;
        margin: 3px 0;
        width: 100%;
      }

      .el-date-editor.el-input,
      .el-date-editor.el-input__wrapper {
        height: 100%;
        width: 100%;
      }

      .el-input,
      .el-input__inner,
      .el-input-number,
      .el-radio-group,
      .el-switch {
        max-width: 100%;

        &.el-date-editor--datetimerange {
          width: 350px !important;
        }
      }
      .el-upload-list--picture-card {
        width: 100%;
      }

      .el-upload-list.el-upload-list--text {
        .el-upload-list__item {
          width: 100%;
        }
      }
      .el-tree {
        --el-tree-node-content-height: 32px !important;
      }
      .el-tree-node__content {
        border-radius: var(--el-border-radius-base);
      }
    }
    &.is-required:not(.is-no-asterisk).asterisk-left
      > .el-form-item__label-wrap
      > .el-form-item__label:before,
    &.is-required:not(.is-no-asterisk).asterisk-left
      > .el-form-item__label:before {
      margin-left: 2px;
      margin-right: unset !important;
    }
  }
  &.disabled {
    :deep(.icon-delete) {
      display: none;
    }
  }
}
</style>
